fix(pm): a population declaration's reason is whole, or the declaration is red - #18660
Merged
Merged
Conversation
…n is red The three population markers in `scripts/pm/dispatch-gates.mjs` capture their reason with `(\S.*)$` under the `m` flag, so the capture ends at the first newline. A reason an author wraps across two or three comment lines was captured as line one only, and no refusal ever asked whether it ended where the author did: `wholeTreePopulationRefusal` checks that a reason EXISTS and that a root walk BACKS it. The seat was handed a sentence that simply stops — and the reason is the one thing a seat reads off that row. The marker grammar now has one spelling (`populationMarkerPattern`), and `populationReasonContinuation` reads, off the same text, the comment line that continues a reason. A continued reason is refused by `populationReasonCutRefusal`, which names the declaration, the file and the line; the whole-tree and wide refusals delegate to it before the checks that read the reason text, and the no-path renderer prints a cut declaration as REFUSED rather than as the fragment it was cut down to. Live derivation over the tree: 27 declarations, one of them cut — `check:objectui-bump` reached the seat as "every path this file writes or reads". Its comment is made whole on one line here, verbatim, so no refusal lands red on main. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
This was referenced Sep 17, 2026
os-justin
marked this pull request as ready for review
September 17, 2026 13:24
os-justin
deleted the
claude/issue-18422-dispatch-gates-whole-reason
branch
September 17, 2026 13:46
This was referenced Sep 17, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…lock comment, and refuses one it cannot read (objectstack-ai#18784) Fixes objectstack-ai#18661 Clause-②: no `scripts/pm/dispatch-gates.mjs`'s three population markers (`no-path-population`, `whole-tree-population`, `wide-population`) built their grammar from one head whose comment-form alternation listed `//` and `#` and nothing else, so a declaration written in a file's own BLOCK-comment idiom parsed as NOTHING — not refused, not printed, not counted, and therefore byte-identical in every channel this tool has to a gate that declares nothing at all. Two live gates were writing one. The author of each believed they had explained why their gate reads no population; every reader of the residue saw their family in the unexamined pile; neither side had anything to check against, which is what makes this class expensive rather than merely wrong. ## The two before-readings (re-derived on this branch, not taken from the card) Taken on `origin/main` `95b21b33be` before any edit: 1. **The population reader answers `null` over both files.** `declaredNoPathPopulation` returns `null` for `scripts/symbol-anchors.mjs` and for `scripts/release-verify-npm.mjs`, and `populationReasonContinuation` returns `null` for both as well — so not even the objectstack-ai#18422 wholeness refusal had anything to grade. Nothing was refused because nothing was read. 2. **Both families sit in `undetermined` with `hints=0` and no annotation.** `node scripts/pm/dispatch-gates.mjs --residue --repo objectstack-ai/objectstack scripts/pm/dispatch-gates.mjs` placed 309 families — 23 matched, 43 undetermined, 225 silent, 7 always-runs, 11 declared-wide — and reported `9 of those 43 undetermined famil(ies) DECLARE that they have no path population`. `node scripts/release-verify-npm.mjs --self-test` and `node scripts/symbol-anchors.mjs --self-test` are both printed in the undetermined block with no `names:` suffix and no `↳ declared no path population` line under them. One line number on the card was stale and is corrected here: `scripts/symbol-anchors.mjs`'s declaration is at `:207` on today's tip, not `:180`; the text is unchanged. `scripts/release-verify-npm.mjs:110` is where the card says. ## ① The grammar: one roster, five forms, and where a reason ENDS in each `MARKER_COMMENT_FORMS` replaces the inline alternation. It is the single place a form is added, the head both marker builders and the continuation reading are derived from, and it classifies each form into one of two KINDS, because the two kinds answer the wholeness question differently: | kind | forms | where the reason ends | |---|---|---| | `line` | `//`, `#` | on the marker line. A comment line under it in the SAME form, carrying text that is not a new `dispatch-gates:` key, is a CUT and is refused — objectstack-ai#18422, unchanged byte for byte | | `block` | `/**`, `/*`, `*` | at the first of: the closing delimiter, a blank star-only line, the next star-`@tag` line, another `dispatch-gates:` key, or EOF. The star-prefixed lines between are JOINED into the reason | The block form's own wholeness question is answered in the header rather than left to be found. Two adjacent `//` lines are two separate comments and nothing in the text says whether the second belongs to the first — that is why objectstack-ai#18660 refuses there, and that refusal is untouched. A `/* ... */` block is ONE comment whose internal newlines are formatting, so its next star line is a continuation *by construction* and the join is decidable from the text rather than guessed. What the block form cannot do is cross any of the three places a block-comment author signals a new thought; each is pinned. A line INSIDE the block carrying text with no star prefix is none of the five endings — it is reason text the walk cannot read — so it is recorded as the block form's CUT and refused with its own remedy ("give that line the block's star prefix, or end the reason before it with a blank star line"), never with the line forms' advice, which would send that author to the wrong half of their declaration. The one residual asymmetry is named in the header rather than hidden: a second sentence on the very next star line, with no blank line between, IS swallowed. That is OVER-inclusion and it reaches a seat as a reason that says too much — visible on the row. The truncation objectstack-ai#18422 refused is UNDER-inclusion and reaches a seat as a sentence that merely ends oddly — invisible. The block idiom's own paragraph break is the text that separates the two, and it is what a block-comment author already writes. ⛔ What a population declaration MEANS is unchanged, and no refusal PR objectstack-ai#18660 added is loosened: the line forms' capture, their continuation reading and their refusal text are all identical, and a self-test case asserts that refusal text still reads "Put the WHOLE reason on the marker line". ## ② The sound: a line that reads like a declaration and did not parse is never silent `unparsedPopulationMarkers` + `unparsedPopulationMarkerRefusal`, RED at author time through this file's own `--self-test` live half, which sweeps every gate source the discovery reads (250 sources on this tree) and names each finding by FILE, LINE and the FORM it was written in — exactly the three things the old output withheld. Two causes, deliberately one finding, because the author's experience is identical: an unrecognised comment form, and a recognised form with no `-- REASON` tail. The chosen place is the refusal rather than the derivation output, because the refusal fires before any row can be printed: an unparsed declaration cannot reach `main`, so a second rendering of a state that cannot exist would be unreachable code, which this file's own rule refuses. Two boundaries, both measured rather than assumed. A line may carry AT MOST ONE comment opener — a line with the docblock's own star plus a second opener is an EXAMPLE of a declaration written inside a comment about declarations, which is how every example in this file is written, and the grammar reads one opener too, so probe and grammar agree about what documentation looks like. And a QUOTE is not a comment opener: every self-test in this family builds its fixtures out of string literals, this file's own included, and the grammar already ignores them for the same reason. ## Census — the whole tree, report-only, no state written Swept over all 8842 tracked files at `95b21b33be`: | reading | count | |---|---| | lines carrying the literal text `dispatch-gates:` | 137 | | of those, lines carrying a POPULATION key | 69 | | parse as a declaration under the OLD form set (`//`, `#`) | 25, across 25 files | | parse under the NEW form set (+ `/**`, `/*`, `*`) | 27, across 27 files | | NEWLY parsing | 2 — `scripts/release-verify-npm.mjs:110`, `scripts/symbol-anchors.mjs:207` | | declaration-SHAPED and unparsed on `main` (what ② would have flagged) | 2 — the same two | | still unparsed after ① | **0** | So ②'s only live cases were the two ① repairs, and the census is its coverage statement: the remaining 67 population-key lines are prose mentions (5 of them, all backticked or mid-sentence), this file's own docblock examples, and string-literal fixtures — none is declaration-shaped, and the live sweep over the tip returns zero. The pins for ② are therefore synthetic plus one live non-vacuity leg that takes a real declaration, re-spells its opener in an unlisted form, and asserts exactly that line is found. ## The family before/after — nothing else moves Same command, same probe path, before at `95b21b33be` and after at `98d0b1dca4`; the full listings differ by 28 lines and every movement is named: - `+ ↳ declared no path population — ...` under `node scripts/release-verify-npm.mjs --self-test` — this change - `+ ↳ declared no path population — ...` under `node scripts/symbol-anchors.mjs --self-test`, with the whole six-line reason joined — this change - `9 of those 43 undetermined` becomes `11 of those 43` — this change - `check-dev-prereqs` 70 declared literals becomes 71, and its `names:` list grows — NOT this change: `scripts/check-dev-prereqs.mjs` moved on `origin/main` in the merge this branch carries - `8842 tracked file(s)` becomes `8844` (two places) — NOT this change: the merge added `.changeset/amplifiers-linked-packages.md` and `scripts/measure-markdown-ts-blocks.mjs` - the derivation header's commit sha The five bucket counts are byte-identical: 23 matched, 43 undetermined, 225 silent, 7 always-runs, 11 declared-wide. No family changed bucket. `no-path-population` annotates WITHIN `undetermined` rather than moving a family out of it, which is what the channel does. ## The ledger row the readable declaration graduates `ROOT_WALK_RESIDUE_LEDGER` carried a hand-written row for `scripts/symbol-anchors.mjs --self-test` — a repo-root walker that declares neither marker and cannot be placed by path. It declared neither marker only because its declaration could not be read. With the form set widened it leaves that population by DECLARING, which the table's own header names as the outcome it exists to push toward, and `check:pm-dispatch-gates` reds on a stale exclusion by design. The row is deleted in the same landing, with a `⚖️` note in the style the table already uses for its one previous graduation. This is the card's defect priced in a second currency: it had cost a hand-maintained exclusion row, carrying by hand the reading the gate's own source already carried. ## Ablation (from the committed fix, restored under a trap) Mutation: the three `kind: 'block'` rows deleted from `MARKER_COMMENT_FORMS`, proven on disk by blob hash — `e72c4d48d33ef670c7baa16f3929cf3f4bf958f5` (== the HEAD blob) becomes `1c2128d800e60ffa93c81cbad76313c381f69c92`. | ablated reading | result | |---|---| | `declaredNoPathPopulation` over the two live files | both back to `null` | | residue's documented-no-population count | 11 back to 9 | | `pnpm check:pm-dispatch-gates` | **exit 1** — `11 of 1809 case(s) failed` | | the 8 block-form grammar pins | all RED | | the live pin "the live tree's BLOCK-form declarations are READ and not dropped" | RED, `found 0: none` | | the SOUND, over the real tree | RED, and it names both specimens: `scripts/release-verify-npm.mjs:110 declares no-path-population in form *` and `scripts/symbol-anchors.mjs:207 declares no-path-population in form /*` | | the ledger case | RED in the OPPOSITE direction — `unlisted: scripts/symbol-anchors.mjs --self-test` — so the row's deletion is coupled to the fix by construction | | the other 1798 cases | green, unchanged | Restore: `git checkout HEAD -- scripts/pm/dispatch-gates.mjs` under an `EXIT INT TERM` trap, verified by hash equality with the HEAD blob AND an empty `git diff HEAD`, not by an exit code. ## Self-test — measured, not NOT MEASURED `pnpm check:pm-dispatch-gates` run DETACHED with its output to a file and read from the file, never under a foreground timeout: **exit 0**, `✓ dispatch-gates self-test: 1809 cases pass`, battery 748.0s on this box, at `71aacfb886`. ## Derived gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from the worktree, no hand-fed path list: 1 path in the change set, 28 commands derived. Every one run, each exit code captured by redirect-then-`$?`, and reconciled: `✓ dispatch-gates --ran: 28 derived famil(ies) accounted for — 28 run, 0 NOT-MEASURED (a DERIVED zero — all 28 recorded an exit code and none of them is 3)` All 28 exit 0. Repo-wide `pnpm lint` (`eslint . --no-inline-config`): exit 0. The derivation at `71aacfb886` prints a STALE TREE note naming one file that moved on `origin/main` afterwards, `scripts/pm/post-stamped.mjs`; the derived command list is byte-identical before and after that move, and the queue rebuilds this PR on the current `main` regardless. ## Scope ⛔ objectstack-ai#18662 is NOT folded in. It asks for the REASON-WHOLENESS reading (the cut refusal) to be extended to `no-check-families` and `inherited-population`; this change gives neither of them one, and `NO_CHECK_FAMILIES_MARKER` is a separate `#`-only regex for YAML that is untouched. One interaction is worth recording: `pathListMarkerPattern` shares the one head with the population markers by construction (objectstack-ai#18673), so `inherited-population` and `self-test-reads` now also accept the block spellings. There are zero live block-form declarations of either key — both live ones are `//` — so nothing moves, and their reason-wholeness exposure is exactly what objectstack-ai#18662 describes, neither widened nor narrowed here. `skip-changeset`: the diff is one file under `scripts/pm/**`, a PM loop tool that no package's `files[]` ships. --- _Generated by [Claude Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…oster the objectstack-ai#18422 reason-wholeness reading (objectstack-ai#18822) Fixes objectstack-ai#18662 Clause-②: no ## The defect `NO_CHECK_FAMILIES_MARKER` and `INHERITED_POPULATION_MARKER`'s reason half captured their reason with the same `(\S.*)$`-under-`m` shape that objectstack-ai#18422 (PR objectstack-ai#18660) repaired for the three population markers: the capture ends at the FIRST NEWLINE, so a reason an author wraps onto the comment line below is read as line ONE and nothing refuses it. Neither marker was in `POPULATION_MARKER_KEYS`, so neither `populationReasonContinuation` nor `populationReasonCutRefusal` could even be asked about them — both threw `unknown population marker key`. No live declaration is cut in this tree today (the census below), so this is exposure, not a live cut; the cost lands on the next author who wraps one and the seat who reads the sentence that stops. ## The before-reading, re-derived on `main` (not taken from the card) Measured against `origin/main` `034f5a3afd`, feeding each marker a reason wrapped over two comment lines — the same wrapped shape objectstack-ai#18422 measured for the population markers. `no-check-families`, consumer `declaredNoCheckFamiliesReason` (the capture at `scripts/pm/dispatch-gates.mjs:2707`, `return m ? m[1].trim() : null;` at :2708): ```text reason read : "steps are an install/build/boot pipeline, and the verdict is" author wrote : "steps are an install/build/boot pipeline, and the verdict is whether the scaffolded app boots at all, which no named local check family covers" consumer sounds? : checkFamilyCoverageGaps -> [] (empty = accepted, silently) wholeness reading: THROWS -- unknown population marker key 'no-check-families' ``` `inherited-population`, consumer `declaredInheritedPopulation` (the reason at :4154, `const reason = m[3].trim();`): ```text population read : [".github/workflows"] reason read : "the workflow directory this module readdirs, and the verdict is" consumer sounds? : no throw, no refusal, no row — the call returned normally wholeness reading: THROWS -- unknown population marker key 'inherited-population' ``` Control, the same wrapped shape on a population marker (objectstack-ai#18422 repaired): ```text populationReasonContinuation(..., 'wide-population', 'scripts/x.mjs') -> {"file":"scripts/x.mjs","line":2,"text":"the count of packages whose manifest this gate refuses","kind":"line"} ``` That is the red this change turns green. ## The registration, and where the refusal prints One grammar, no second pattern, no hand-rolled continuation reader. - `MARKER_KEY_FORMS` / `markerLineHead(key)` — the shared head, built from `MARKER_COMMENT_FORMS` FILTERED to the forms that key's language has. - `REASON_TAIL_MARKER_KEYS = [...POPULATION_MARKER_KEYS, 'no-check-families']` — `no-check-families` now comes out of `populationMarkerPattern` instead of the fourth hand-written copy of the same grammar it was. The builder keeps its name because `population*` is what this machinery is called where it is exported; the roster, not the name, is the authority on which keys it serves, and the docblock says so. - `MARKER_REASON_GRAMMARS` — which builder reads each key and which capture group holds its reason, derived BY CONSTRUCTION from the two builders' own key rosters rather than hand-listed. That is what closed the class: a key cannot be added to either builder without the wholeness reading arriving with it. It is also why `self-test-reads` — the sixth reason-bearing key, same grammar, same defect, filed under objectstack-ai#18673 and NOT named on this card — is covered in the same line rather than becoming a third card on this file. See "Scope" below. - `readPopulationMarker` reads its reason out of the group the roster names, and returns the raw match so a path-list reader takes its path list off the SAME read its reason came from. - `markerReasonCutRefusal(markerKey, cut)` — the refusal TEXT, one copy, five keys. `populationReasonCutRefusal(entry, markerKey)` is now that function reached through a discovery entry, and its output is byte-identical to what it was before the split (pinned). Where it prints: the three population channels carry their declaration into a discovery entry that IS rendered, so their refusal is a printed row. These markers have no such row (see the next section), so the READ refuses — `refuseCutMarkerReason` throws, the CLI catches it at its `catch (err)` and prints `dispatch-gates: derivation failed —` followed by the refusal text, exit 2. That is already how both path-list markers refuse an invented path. The message names the file, the line, the marker and the continuation it saw: ```text dispatch-gates: .github/workflows/scaffold-e2e.yml declares no-check-families and its reason does not END on the marker line: .github/workflows/scaffold-e2e.yml:7 continues it with "whether the scaffolded app boots at all, which no named local check family covers". The capture stops at the FIRST NEWLINE, so the seat is handed the declaration cut off mid-sentence — ... Put the WHOLE reason on the marker line, however long it runs ... ⛔ Never widen the marker to swallow the next line instead ... ``` The second surface is this file's own `--self-test` live census, below — the same place the population markers' live half reds. ⛔ What either declaration MEANS is unchanged: the `inherited-population` path list is untouched, its invented-path refusal is unchanged and pinned independently, `no-check-families` still exempts exactly the workflows it exempted, and no refusal PR objectstack-ai#18660 / PR objectstack-ai#18784 added is loosened. ## The card's 「not measured」, answered: NO, neither consumer renders the reason Read to the surface the text lands on, at `034f5a3afd`: - `declaredNoCheckFamiliesReason` has exactly ONE consumer — `checkFamilyCoverageGaps` at :4298, `if (declaredNoCheckFamiliesReason(text)) continue;`. The return value is read as a BOOLEAN. The reason string reaches no row, no log and no seat. - `declaredInheritedPopulation` has three production call sites (`discoverFamilies`' `hintsOfModule`, and two in this file's own self-test) plus the governed-read census; every one of them reads `.population`. The only read of `.reason` in the tree is a self-test case asserting it is non-empty. So a cut costs nothing to a RENDERING today. The refusal is owed anyway and is stated in both docblocks rather than hidden: wholeness is a property of the declaration, not of today's consumer, and the next reader of either reason is the seat that greps the workflow or the module for it — which is exactly the reader the population markers' repair was written for. The cost is that this refusal buys a seat's future read, not a row on today's output. ## PR objectstack-ai#18784's block-comment forms, measured against these two Measured both directions, one form per row, on `034f5a3afd` and on this branch. BEFORE — `MARKER_COMMENT_FORMS` reached `no-check-families` in NO form at all: its pattern was a hand-written regex that never consulted the roster, so `#` matched because it was hard-coded there and the other four did not. It reached `inherited-population` in ALL FIVE forms already (that marker is built on the shared head since objectstack-ai#18673/objectstack-ai#18661) — but its reason was `m[3].trim()`, so a block-form reason kept the closing delimiter and a wrapped block reason was cut at the first newline: ```text BEFORE no-check-families: # -> "a reason */" // -> null /* -> null /** -> null * -> null BEFORE inherited-population: # -> "a reason */" // -> "a reason */" /* -> "a reason */" block WRAPPED reason -> "the workflow directory this module readdirs, and" (cut) ``` AFTER — `no-check-families` is built from the roster FILTERED to `#`, and the four other forms stay null. ⛔ That is not a widening withheld; it is the answer to the question: **the block form cannot apply to a YAML `#` marker.** `#` is the only comment syntax YAML has — a `//` or slash-star line in a workflow is document content, not a remark, so reading a declaration off one would be reading it off text the workflow's own parser never treats as a comment. The `inherited-population` rows are unchanged in form coverage; only its block-form reason now ends where the block ends: ```text AFTER no-check-families: # -> "a reason */" // -> null /* -> null /** -> null * -> null AFTER inherited-population: # -> "a reason */" // -> "a reason */" /* -> "a reason" block WRAPPED reason -> "the workflow directory this module readdirs, and every other literal here is a join base" (whole) ``` ## Scope — and the sixth key The card names two markers. The registration is derived from the two grammar builders' key rosters, so it also reaches `self-test-reads` (objectstack-ai#18673), the sixth reason-bearing key. That is deliberate, and it is the bounded in-place fix rather than scope creep — the four conditions, answered: (1) same defect class as this card, the identical first-newline capture; (2) mechanical, and the shape is pinned by the two markers this card names; (3) no other claim holds this file (28 open PRs' file lists read at dispatch time — none touches it; objectstack-ai#18536's machine-side half is queued behind this card, not claimed); (4) same gate family, no new verification surface. The alternative was a roster that hand-lists five of six reason-bearing keys — a second copy of "which keys have a reason", wrong in the silent direction the moment a seventh arrives, which is exactly how these two sat outside objectstack-ai#18422 for two cards. ⛔ objectstack-ai#18661 / PR objectstack-ai#18784 is read and not re-opened: its form roster and its dropped-declaration sound are untouched. ⛔ objectstack-ai#18536's machine-side half is not folded in — nothing in this diff touches half-state or Clause-② machinery. ## Pins — 26 new cases in `--self-test` Per marker (`no-check-families`, `inherited-population`, `self-test-reads`): - a wrapped-reason case REFUSED by name, asserting the message carries the file, the `file:line` of the continuation, the marker key and the continuation's text — the refusal text pinned, not paraphrased; - a whole-reason control that still reads its reason (and, for `no-check-families`, that the workflow is still not a coverage gap) — the repair refuses a cut, it does not refuse the marker. Plus: the refusal reaching `checkFamilyCoverageGaps`, the marker's one consumer; a blank-line-separated comment still not a continuation (the terminator all six live declarations write); `inherited-population`'s invented-path refusal pinned as INDEPENDENT of the cut refusal; `self-test-reads`' missing-read-set refusal still firing first; the `#`-only restriction both ways, plus the restriction narrowing-only and its drift guard driven through a bad table; the wholeness roster pinned equal to the two builders' rosters and named at all six keys; the reason GROUP pinned per builder; and `populationReasonCutRefusal` pinned byte-equal to `markerReasonCutRefusal` so the split cannot drift. The population markers' existing pins are byte-unchanged. ## The census — the five (six) live declarations, measured whole Taken in `--self-test` against the real tree at the head on every run, not printed once into this body. Each row is NAMED, never counted: ```text .github/workflows/merged-branch-reaper.yml:212 no-check-families .github/workflows/os-create-smoke.yml:48 no-check-families .github/workflows/scaffold-e2e.yml:23 no-check-families scripts/cli-build-prerequisite.mjs:111 inherited-population scripts/pm/check-expected-skips.mjs:131 self-test-reads scripts/pm/dispatch-gates.mjs:702 inherited-population ``` The card's five, plus the `self-test-reads` declaration the sixth key brings. All six are one-liners followed by a blank line — no reason is cut in this tree today, which is the card's own reading, re-taken. The case asserts three things separately: the roster is exactly those six, every reason ENDS on its marker line, and every reason is non-empty. A non-vacuity control puts a continuation under a LIVE declaration and asserts that exact file is refused by name, so the census cannot pass by measuring nothing. ## Ablation From the COMMITTED fix, the registration removed on disk — the three `refuseCutMarkerReason(...)` calls, which is the mutation shape this file already names for this family ("deleting the `populationReasonCutRefusal` call from either refusal"). Mutation proven by blob hash before the run, restored by hash under a `trap ... EXIT INT TERM` after it: ```text head : 0538995 head blob : 1d4c953 on-disk before : 1d4c953 anchor count before: 3 (expect 3) anchor count after : 0 (expect 0) on-disk after : 5ea16c9a050769027d876e39f8b7a0d21c439a03 MUTATION: PROVEN — blob hash moved and 3 anchor line(s) left the file ✗ dispatch-gates self-test: 5 of 1835 case(s) failed. os-verify-lock: VERDICT command-exit 1 · held the lock 745s (12m25s) · waited 1s RESTORE: hash-on-disk=1d4c95393c3749e68129dc2688e8df8c351aaa9b head-blob=1d4c95393c3749e68129dc2688e8df8c351aaa9b RESTORE: OK — byte-identical to HEAD RESTORE: git diff HEAD (must be empty): [empty] ``` The five, and only the five, are the new refusal pins — one per marker, plus the consumer-reaching case and the census non-vacuity control: ```text ✗ a no-check-families reason that does not END on the marker line is REFUSED, naming the workflow, the line, the marker and the continuation ✗ and the refusal reaches the ONE consumer this marker has — the boolean read in checkFamilyCoverageGaps refuses rather than accepting half a sentence ✗ an inherited-population reason that does not END on the marker line is REFUSED, naming the module, the line, the marker and the continuation ✗ the census is not vacuous over this tree: put a continuation under a LIVE declaration and exactly that file is refused, by name ✗ a self-test-reads reason that does not END on the marker line is REFUSED too, in the same words and naming the same four things ``` Nothing pre-existing reds. The continuation READING, the whole-reason controls, the roster pins and every population-marker pin stay green under the ablation — which is the point: this change registers a refusal, it does not change how a reason is read. ## The self-test line Run DETACHED with output to a file and waited on in the foreground (`tail --pid` on the detached pid), never under a foreground timeout, and under the shared verify lock: ```text ✓ dispatch-gates self-test: 1835 cases pass. os-verify-lock: VERDICT command-exit 0 · held the lock 734s (12m14s) · waited 0s ``` 1809 before this change, 1835 after — 26 new cases. Shared-box seconds, not idle-box figures. ## Derived gates, each with its exit code Derived from the worktree with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no hand-fed path list), every command run with the exit code captured by redirect-then-`$?`, then reconciled with `--ran`. ```text node scripts/check-ci-filter-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs --self-test :: exit 0 node scripts/check-scripts-symbol-anchors.mjs :: exit 0 node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0 node scripts/check-self-test-wired.mjs :: exit 0 node scripts/check-self-test-wired.mjs --self-test :: exit 0 node scripts/check-self-test-workflow-commands.mjs :: exit 0 node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0 node scripts/check-whole-set-label-write.mjs :: exit 0 node scripts/check-whole-set-label-write.mjs --self-test :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:bash32-floor :: exit 0 pnpm check:cli-command-ids :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:declared-population-live :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:entry-guard :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:parse-guard :: exit 0 pnpm check:pm-dispatch-gates :: exit 0 (the detached run above) pnpm check:pnpm-filter-targets :: exit 0 pnpm check:ratchet-remedy-authority :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:watch-hint-literal :: exit 0 ``` ```text ✓ dispatch-gates --ran: 28 derived famil(ies) accounted for — 28 run, 0 NOT-MEASURED (a DERIVED zero — all 28 recorded an exit code and none of them is 3). ``` Repo-wide `pnpm lint` (`eslint . --no-inline-config`): exit 0. `skip-changeset`: the diff is one file under `scripts/pm/`, which no package's `files[]` ships — nothing published moves. ## Acceptance notes To file (class (c), a declaration the tool silently drops), NOT fixed here — different defect class from this card, so the bounded in-place exemption does not apply: **the dropped-declaration probe `unparsedPopulationMarkers` (objectstack-ai#18661) is keyed on `POPULATION_MARKER_KEYS` alone**, so a line that READS as a `no-check-families`, `inherited-population` or `self-test-reads` declaration and does not PARSE as one makes no sound at all — no refusal, no row, no count — while the identical shape on a population key is reported. Measured on `034f5a3afd`, three dropped spellings returned `[]` from the probe with a population-key control lighting on the same opener. The `self-test-reads` instance re-opens objectstack-ai#18673's own hole: a dropped declaration takes a family out of the derived set, and the `--ran` reconciliation then says "0 NOT-MEASURED" about a set that no longer contains it. Dedupe words: `unparsed marker probe roster` · `dropped declaration silent` · `POPULATION_MARKER_LOOKALIKE keys` · `inherited-population dropped` · `self-test-reads dropped`. Noted, not filed: routing `inherited-population`'s block-form read through `blockFormReason` also drops the closing delimiter that `m[3].trim()` used to leave in the reason (`"a reason */"` becomes `"a reason"`). No live declaration is written in a block form, so nothing moved in this tree; it is measured in the block-form table above rather than left to be found. Next reader of this file: objectstack-ai#18536's machine-side half. --- _Generated by [Claude Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18422
The defect
The three population markers in
scripts/pm/dispatch-gates.mjs—NO_PATH_POPULATION_MARKER,WHOLE_TREE_POPULATION_MARKERandWIDE_POPULATION_MARKER— each captured their reason with(\S.*)$under themflag, so the capture ends at the first newline; a reason an author wraps across two or three comment lines was captured as line one only. Nothing refused it: read line by line,wholeTreePopulationRefusalchecks that a reason EXISTS, that no sibling marker contradicts it, and that a root walk BACKS it — never that it is whole, and there was no other caller that did. The row rendered as a sentence that merely stops, with nothing red at author time or at read time, and the reason is the one thing a seat reads off that row when deciding whether a family belongs on its card. Measured on #17472 / PR #18414; measured again live in this tree, below.All three markers shared the shape (
WIDE_POPULATION_MARKERincluded — measured, not assumed), so the contract lands on all three.Contract, before and after
(\S.*)$underm— ends at the first newlinedispatch-gates:declarationalwaysRunsPopulationLinesand the--jsonrefusedfield for whole-tree and wide, the undetermined listing for no-pathThe grammar now has ONE spelling (
populationMarkerPattern), because the continuation reading has to agree with the capture about what a marker line is, down to the comment form; two spellings of one grammar drift silently.populationReasonContinuationreads, off the same text, the comment line that continues a reason;readPopulationDeclarationrecords the reason and its continuation from ONE source and ONE file, so a refusal can never grade file A's reason against file B's continuation;populationReasonCutRefusalis the refusal, shared by all three channels, and the whole-tree and wide refusals delegate to it.Ordering, deliberate and pinned both ways: the cut is refused after the two-marker pair refusals (a declaration that contradicts a sibling is refused for THAT, in the words a reader has been getting for it) and before the whole-tree walk check and the wide hint check — both of those grade the declaration against a reason this reading says is only part of one, so a hint named in the wrapped half would read as unaccounted for and the refusal would name the wrong defect in confident words.
Shape B, and why — the four axes
Shape B (refuse at author time) over shape A (consume a comment block).
check:route-envelope), and exactly one wrapped and being cut today. The one-line reason is what this convention already IS; the pull is for the one declaration to be made whole, not for a multi-line grammar nobody in this tree writes.No axis conflicts, so there is no trade-off to hand up. The triage boundary (5713131801) is held: this is the capture domain and its validation, ⛔ not a marker that matches arbitrary multi-line text.
The pins
The file's
--self-testregisters cases witht(name, cond, detail)into one flatcasesroster and holds theSELF_TEST_VERDICThandshake the dispatch refuses without; perdocs/audits/2026-09-self-test-shape-census.md:377its floor isNONEand its handshake isHELD— a recorded shape, unchanged here.Registered: 25 new cases (fixture battery
A declaration's reason is WHOLE, or the declaration is RED (#18422), plus three live-half pairs). Whole file after:✓ dispatch-gates self-test: 1771 cases pass.What they pin, clause by clause:
git add -Aover a worktree inside the checkout stages a mode-160000 gitlink at exit 0 with a warning — no repo-side gate refuses a gitlink that has no.gitmodulesrow #17472 first-draft shape as a case — the capture still ends at the first newline (pinned as the defect, not as a claim it went away), and the wholeness reading names the continuation line;dispatch-gates:key ⇒ not a continuation (it is a second declaration; the pair refusals grade that shape);#line under a//declaration is not a comment in that language;The live derivation, over the tree at this head
Every declaration the three markers match today, run through the new contract:
One declaration reds — and it was being cut on
mainright now, not hypothetically:check:objectui-bumpreached the seat asevery path this file writes or reads, 36 characters of a 466-character reason wrapped over six comment lines. A refusal cannot land red onmain, so the declaration is made whole on ONE line in this same PR:scripts/bump-objectui.selftest.sh:50— comment-only edit, the author's words joined verbatim at the wrap points (the join is asserted byte-for-byte against the original lines, not retyped).pnpm check:objectui-bumpexit 0 after it.After the repair: 27 declarations, 0 refused. The count is not vacuous — the refusal fired on this tree before the repair (above), and three live-half cases in the self-test re-fire it per channel by putting a continuation on a live entry and asserting the refusal names it.
Ablation, from the committed fix
Mutation: the refusal deleted —
populationReasonCutRefusalreturns null once it has a declaration, which is the pre-fix state exactly (the cut is detectable and nothing is red).Predicted direction before the run: 转红. Observed: 转红, 8 cases, and they are exactly the ones the refusal buys —
The controls stayed GREEN under the same mutation, which is what makes these an instrument rather than a restatement: the two-marker pair refusals,
a WHOLE reason is refused nothing on any of the three, every continuation-reading case (the detector still works — it is the refusal that was deleted, which is the card's whole point: the cut is knowable and nothing is red), and the threeevery live … reason ENDS on its own marker linecases (0 cuts in the tree either way).Run under
trap '<restore>' EXIT INT TERMwith an absoluteREPO_ROOT, restored withgit checkout HEAD -- <path>(never the bare form, which takes the mutation back out of the index), and the restore proven by blob hash AND by an emptygit diff HEAD— not by an exit code.Gates
Derived from the tree with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no hand-fed path list; the tool takes its own change set off the merge base), every command run, each exit code captured redirect-then-$?, reconciled with--ran.30 derived, 30 run, 0 NOT-MEASURED, 0 UNRUN — every one exit 0.
pnpm check:pm-dispatch-gatesis this file's own--self-testand is HEAVY:✓ dispatch-gates self-test: 1771 cases pass., 633.9s, run DETACHED per this file's own header (#14281) and underscripts/pm/os-verify-lock.sh(VERDICT command-exit 0 · held the lock 635s · waited 0s). It exceeds the ~10-minute foreground cap, which is why the header says to detach it.origin/mainmoved three commits under this branch mid-run, two of them touching gate scripts, so the derivation was re-run from a scratch worktree atorigin/main84ad2e139against these two paths: the family list came back byte-identical to the one above — no family was added by the drift.pnpm lintrepo-wide, not narrowed: exit 0 in 84s (node --stack-size=4000 eslint . --no-inline-config, at3f4bf6b01). The narrowing this lane has been using was not needed on this run.skip-changeset:scripts/pm/**andscripts/bump-objectui.selftest.share in no package'sfiles[]— nothing published moves.Out-of-scope findings (⛔ not fixed here)
Both are the same CLASS as this card — an author writes a declaration and the consumer silently drops part or all of it — and both are OUTSIDE the file surface triage drew (5713131801), so they are named here for the seat rather than repaired in this PR.
(?:\/\/|#), so a declaration written inside a BLOCK comment parses as nothing at all. Two live specimens:scripts/symbol-anchors.mjs:180(/* dispatch-gates: no-path-population -- …) andscripts/release-verify-npm.mjs:110(* dispatch-gates: no-path-population -- …). Measured:declaredNoPathPopulationreturnsnullfor both, both families sit inundeterminedwithhints=0, and their authors' examined-and-explained status is dropped with no tell — the residue counts them with the families nobody has looked at, which is the exact bucket the marker exists to split. Dedupe words:block comment marker,no-path-population unparsed,symbol-anchors declaration,release-verify-npm population,comment form alternation.NO_CHECK_FAMILIES_MARKER(workflow-level, read bydeclaredNoCheckFamiliesReason) andINHERITED_POPULATION_MARKER(module-level, itsreasonhalf). Neither is cut in the tree today (measured: the three liveno-check-familiesdeclarations and bothinherited-populationdeclarations are one-liners followed by a blank line), so this is exposure, not a live defect. The repair is one line each: register the key inPOPULATION_MARKER_KEYS/POPULATION_DECLARATION_FIELDSand read the continuation — the helper this PR adds is generic and was deliberately built so the class closes in one move. Dedupe words:no-check-families reason cut,inherited-population reason,population marker wholeness,first newline capture,dispatch-gates marker roster.Acceptance notes
noted, not filed: this file's--self-testhas no per-battery floor roster (AGENTS.md 「Writing a--self-test」 asks for battery name → minimum case count); it registers into one flatcasesroster. Already recorded —docs/audits/2026-09-self-test-shape-census.md:377grades it floorNONE, handshakeHELD. Not a finding, a censused state. 承接者: whoever works that census row.🤖 Generated with Claude Code
https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Generated by Claude Code